/**
 * @file Dinucleotide.java
 * @author: Bon William <william.bon2@gmail.com>
 * @author: Ernewein Audrey <audrey.ernewein@gmail.com>
 * @brief:
 * This file contains the static functions used for generating all the dinucleotides' sets
 * with user's asked parameters (length, maximal and self complementary).
 */
package generator;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class DinucleotideSet implements Comparable<DinucleotideSet>{
	
	/* Contains elements of a set (dinucleotides) */
	public ArrayList <String> elements = new ArrayList <String>();
	
	/* constructors */
	public DinucleotideSet() {
	}
	
	public DinucleotideSet (String nom)
	{
		this.elements.add(nom);
	}
	
	public DinucleotideSet (ArrayList <String> noms)
	{
		this.elements=new ArrayList<String>(noms);
	}
	
	/**
	 * @function equals 
	 * Override equals function used to compare two sets.
	 * This function is automatically called by contains function.
	 * */
	@Override public boolean equals(Object other)
	{
	    boolean result = true;
	    /* Check that this function is used on Dinucleotide class only */
	    if (other instanceof DinucleotideSet)
	    {
	    	DinucleotideSet that = (DinucleotideSet) other;
	        /* Check if elements of the set are the same */
	        for(int i=0;i<this.elements.size();i++)
	        {
	        	if(this.elements.get(i)!=that.elements.get(i))
	        	{
	        		/* We found two different elements, the sets are not the same */
	        		result=false;
	        	}
	        }
	    }
	    return result;
	}
	
	/**
	 * function listOfDinucleotideSets
	 * Generate all sets of Dinucleotides with length elements.
	 * This function must only be used with this program because it already
	 * don't include some sets that will never be used (small filter).
	 * 
	 * The small filter check that no set will include a dinucleotide and his complementary
	 * 
	 * @param length the length of Dinucleotide sets to be generated
	 * @return An ArrayList containing all sets of dinucleotides
	 */
	public static ArrayList <DinucleotideSet> listOfDinucleotideSets(int length)
	{
		/* List of all dinucleotides that can be added */
		String [] tabAllNucleotide = {"AC", "AG", "AT", "CA", "CG",
				"CT","GA", "GC", "GT", "TA", "TC", "TG"}; 
		
		/* dictionary matching complementary of all dinucleotides */
		Map<String, String> dictionary = new HashMap<String, String>();
		dictionary.put("AC","CA");
		dictionary.put("AG","GA");
		dictionary.put("AT","TA");
		dictionary.put("CG","GC");
		dictionary.put("CT","TC");
		dictionary.put("GT","TG");
		dictionary.put("CA","AC");
		dictionary.put("GA","AG");
		dictionary.put("TA","AT");
		dictionary.put("GC","CG");
		dictionary.put("TC","CT");
		dictionary.put("TG","GT");

		
		ArrayList <DinucleotideSet> tabDinucleotide = new ArrayList <DinucleotideSet>();
		ArrayList <DinucleotideSet> tabres = new ArrayList <DinucleotideSet>();
		
		int k = 1;
		if(length < 1)
		{
			/* Must never be in this test or program has been hacked */
			System.out.println("The sets' length must be positive.");
			System.exit(-1) ;
		}
		/* We first add the simple dinucleotides in the array, creating the first sets */
		for(int i=0; i<tabAllNucleotide.length; i++)
		{
			tabDinucleotide.add(new DinucleotideSet(tabAllNucleotide[i]));
		}
		
		/* Loop for sets' generation */
		while(length > k)
		{
			/* Temporary array cleaning */
			tabres.clear();
			
			/* Loop on each existing sets */
			for(int i=0; i<tabDinucleotide.size();i++)
			{
				/* For each sets of length k-1, we create all the sets of length k that
				 * are the original set which is added a dinucleotide */
				/* Loop on each existing dinucleotides */
				for(int j=0; j<tabAllNucleotide.length; j++)
				{
					ArrayList<String> elements= new ArrayList<String> (tabDinucleotide.get(i).elements);
					
					/* First simple filter is applied there,
					 * we check that the dinucleotide is not already in the set neither is
					 * it's complementary */
					if(!tabDinucleotide.get(i).elements.contains(dictionary.get(tabAllNucleotide[j])) &&
							!tabDinucleotide.get(i).elements.contains(tabAllNucleotide[j]))
					{
						/* we add the dinucleotide, creating a new bigger set */
						elements.add(tabAllNucleotide[j]);
						Collections.sort(elements);
						tabres.add(new DinucleotideSet(elements));
					}
				}
			}
			/* Flushing answer array and moving elements from temporary to final array */
			tabDinucleotide.clear();
			for(int i=0; i<tabres.size();i++)
			{
				if(!tabDinucleotide.contains(tabres.get(i)))
					tabDinucleotide.add(tabres.get(i));
			}
			k++;
		}
		
		/* Sorting all sets */
		for(int i=0;i<tabDinucleotide.size();i++)
		{
			Collections.sort(tabDinucleotide.get(i).elements);
		}
		Collections.sort(tabDinucleotide);
		return tabDinucleotide;
	}
	
	/**
	 * @function cleanCC
	 * This function apply a filter on a dinucleotide set list, removing
	 * those who are not circular codes.
	 * 
	 * @param list the dinucleotide sets' list to be filtered
	 */
	public static void cleanCC(ArrayList<DinucleotideSet> list)
	{
		int i=0;
		/* loop on each set */
		while (i<list.size())
		{
			/* 5-Necklace test:
			 * if the set has a 5-Necklace, it's not a circular code and
			 * we remove it from the sets' list
			 * */
			if (has5Necklace(list.get(i)))
			{
				list.remove(i);
			}
			else
			{
				i++;
			}
		}
	}
	
	/**
	 * @function comp
	 * Match a nucleotide with it's complementary
	 * 
	 * @param in the nucleotide
	 * @return the nucleotide's complementary
	 */
	public static char comp(char in)
	{
		char c='A';
		switch (in)
		{
			case 'A': c= 'T';break;
			case 'C': c= 'G';break;
			case 'G': c= 'C';break;
			case 'T': c= 'A';break;
		}
		return c;
	}
	
	/**
	 * @function isSelfComplementary
	 * Test whether a dinucleotide set is self complementary or not
	 * @param d the dinucleotide set to test
	 * @return true if the dinucleotide set is self complementary.
	 */
	static boolean isSelfComplementary(DinucleotideSet d)
	{
		/* Loop on each dinucleotide of the set */
		for(int i=0;i<d.elements.size();i++)
		{
			/* Building dinucleotide's complementary */
			String tmp=d.elements.get(i);
			char[] tab=tmp.toCharArray();
			char[] newTab= new char[2];
			newTab[0]=comp(tab[1]);
			newTab[1]=comp(tab[0]);
			String tmp2=new String(newTab);

			if(!d.elements.contains(tmp2))
				return false;
		}
		return true;
		
	}
	
	/**
	 * @function cleanSC
	 * A small filter that remove sets that are not self complementary
	 * 
	 * @param list the DinucleotideSet list
	 */
	public static void cleanSC(ArrayList<DinucleotideSet> list)
	{
		int i=0;
		while (i<list.size())
		{
			/* Test if the dinucleotide set is self complementary */
			if (!isSelfComplementary(list.get(i)))
			{
				list.remove(i);
			}
			else
			{
				i++;
			}
		}
	}
	
	/**
	 * @function isMaximal
	 * Test whether the dinucleotide set is maximal or not
	 * @param d the dinucleotide set
	 * @return true is the dinucleotide set is maximal
	 */
	public static boolean isMaximal(DinucleotideSet d)
	{
		String [] tabAllNucleotide = {"AC", "AG", "AT", "CA", "CG", "CT","GA", "GC", "GT", "TA", "TC", "TG"}; 
		/* a dinucleotide set of length 6 is always maximal */
		if (d.elements.size()==6)
			return true;
		
		/* Look if we can add an other nucleotide which one the set would be
		 * always a circular code */
		/* Loop on each existing dinucleotide*/
		for(int i=0;i< tabAllNucleotide.length;i++)
		{
			if(!d.elements.contains(tabAllNucleotide[i]))
			{
				/* We add the element and perform a 5-Necklace test */
				d.elements.add(tabAllNucleotide[i]);
				boolean test=has5Necklace(d);
				d.elements.remove(tabAllNucleotide[i]);
				if(!test)
					return false;
			}
		}
		return true;
	}
	
	/**
	 * @function cleanMax
	 * A small filter that remove non maximal dinucleotides set in the list.
	 * @param list the dinucleotide set list
	 */
	public static void cleanMax(ArrayList<DinucleotideSet> list)
	{
		int i=0;
		/* Loop on each dinucleotide set */
		while (i<list.size())
		{
			if (!isMaximal(list.get(i)))
			{
				/* if the dinucleotide set is not maximal, we remove it from the list */
				list.remove(i);
			}
			else
			{
				i++;
			}
		}
	}
	
	/**
	 * @function generate
	 * Generate the fully defined dinucleotide set with wanted options and length
	 * 
	 * @param length the dinucleotides' sets' length
	 * @param max must the dinucleotides' sets be maximals ?
	 * @param selfC must the dinucleotides' sets be self complementary
	 * @return and array of all wanted dinucleotides' sets
	 */
	public static ArrayList <DinucleotideSet> generate(int length, boolean max, boolean selfC, String workingDir)
	{
		/* Generate first all dinucleotides with wanted length
		 * (the first small filter is applied, see listOfDinucleotideSets).*/
		ArrayList <DinucleotideSet> res = listOfDinucleotideSets(length);
		String fileName = new String();
		/* Clear all the non circular codes from the list */
		cleanCC(res);
		
		/* Removing the non maximal sets if wanted */
		if(max)
		{
			cleanMax(res);
		}
		
		/* Removing the non self complementary sets if wanted */
		if(selfC)
		{
			cleanSC(res);
		}
		
		/* formating the name of the file to be written */
		if(!max && !selfC)
			fileName = "ListCCLg" + length + ".txt";
	
		else if(!max && selfC)
			fileName = "ListCCSLg" + length + ".txt";
		
		else if(max && !selfC)
			fileName = "ListCCMLg" + length + ".txt";
		
		else if(max && selfC)
			fileName = "ListCCMSLg" + length + ".txt";
		
		/* Writing the file containing all the wanted sets */
		writeFile(res,workingDir+fileName,"");
		
		return res;
	}
	
	/**
	 * @function generateAll
	 * generate all dinucleotides sets with length between 1 and 6
	 * and that are Circular Code, Self Complementary Circular Code, Maximal Circular Code
	 * and Self Complementary Maximal Circular Codes.
	 */
	public static void generateAll(String workingDir)
	{
		String numberC = new String();
		ArrayList <DinucleotideSet> res = new ArrayList<DinucleotideSet>();

		//Loop on different length for generating Circular Codes
		for(int i=1; i<=6; i++)
		{
			res = generate(i,false, false,workingDir);
			numberC += res.size();
			if(i<6)
				numberC+=" ";
		}
		/* Writing number of sets generated */
		writeFile(res, workingDir+"NbCC.txt", numberC);
		numberC = "";
		
		//Self Complementary Circular Codes
		for(int i=1; i<=6; i++)
		{
			res = generate(i,false, true, workingDir);
			numberC += res.size();
			if(i<6)
				numberC+=" ";
		}
		writeFile(res, workingDir+"NbCCS.txt", numberC);
		numberC = "";
		
		//Maximal Circular Codes
		for(int i=1; i<=6; i++)
		{
			res = generate(i,true, false, workingDir);
			numberC += res.size();
			if(i<6)
				numberC+=" ";
		}
		writeFile(res, workingDir+"NbCCM.txt", numberC);
		numberC = "";
		
		//Self Complementary and Maximal Circular Code
		for(int i=1; i<=6; i++)
		{
			res = generate(i,true, true, workingDir);
			numberC += res.size();
			if(i<6)
				numberC+=" ";
		}
		writeFile(res, workingDir+"NbCCMS.txt", numberC);
		numberC = "";
		
	}

	/**
	 * @function writeFile
	 * write a file in the current working directory
	 * @param list the list of dinucleotides' set to write if any
	 * @param fileName the file name to create or replace
	 * @param number The number of generated sets
	 */
	public static void writeFile(ArrayList<DinucleotideSet> list, String fileName, String number)
	{
		try{
			File file = new File(fileName);
			file.createNewFile();
			
			try{
				FileWriter fstream = new FileWriter(file);
				BufferedWriter out = new BufferedWriter(fstream);
				/* Do we want to write all sets or just a brief with sets number */
				if(number.isEmpty())
				{
					/* We are writing all sets */
					for(DinucleotideSet i : list)
					{
						for(int k=0;k<i.elements.size();k++)
						{
							out.write(i.elements.get(k));
							if(k!=(i.elements.size()-1))
								out.write(" ");
						}
						out.write("\n");
					}
				}
				else
					out.write(number + "\n");
				out.close();
			}
			catch(FileNotFoundException e){
				System.out.println("Cannot find file.");
			}		
		}
		catch(IOException e){
			System.out.println("Cannot create the file, check for permissions.");
		}		
	}
	
	@Override
	/**
	 * @function compareTo
	 * override the common function compareTo.
	 * This function is automatically called by contains function.
	 */
	public int compareTo(DinucleotideSet arg0) {
		/* Compare one by one the elements */
		for(int i=0;i<this.elements.size();i++)
		{
			int comp = this.elements.get(i).compareTo(arg0.elements.get(i));
			if(comp!=0)
				return comp;
		}
		return 0;
	}
	
	/**
	 * @function has5Necklace
	 * Test whether the dinucleotide set has a 5-Necklace.
	 * @param d the dinucleotides' set
	 * @return true if it has a 5-Necklace
	 */
	public static boolean has5Necklace(DinucleotideSet d)
	{
		ArrayList<String> necklaces = new ArrayList<String>();
		ArrayList<String> tmpNecklaces = new ArrayList<String>();
		
		String [] Alphabet = {"A","C","G","T"};
		necklaces.add("A");
		necklaces.add("C");
		necklaces.add("G");
		necklaces.add("T");
		/* We generate all necklaces with a length of 5 */
		for(int i=0;i<4;i++)
		{
			for(int j=0;j<necklaces.size();j++)
			{
				for(int k=0;k<Alphabet.length;k++)
				{
					String tmp=necklaces.get(j)+Alphabet[k];
					tmpNecklaces.add(tmp);
				}
			}
			necklaces.clear();
			for(int j=0;j<tmpNecklaces.size();j++)
			{
				necklaces.add(tmpNecklaces.get(j));
			}
			tmpNecklaces.clear();
		}
		
		/* Test each necklace */
		for(int i=0;i<necklaces.size();i++)
		{
			boolean result=true;
			for(int j=0;j<4;j++)
			{
				String element=necklaces.get(i).substring(j,j+2);
				if(!d.elements.contains(element))
					result = false;
			}
			if(result)
				return result;
		}
		return false;
	}
}
